Practical Tips
This page collects small practical notes for writing Tensorial code clearly and efficiently.
- Use concrete tensor field types
- Put known symmetry in the type
- Specify
@einsumresult types when needed - Flatten at boundaries
- Type inference when unpacking
Use concrete tensor field types
Tensor constructors can infer the tensor order N and the number of independent components L:
julia> @Tensor{Tuple{@Symmetry{3,3}}}SymmetricSecondOrderTensor{3, T, 6} where T (alias for Tensor{Tuple{Symmetry{Tuple{3, 3}}}, T, 2, 6} where T)
When a tensor type appears as a struct field, include all type parameters so the field type is concrete:
julia> isconcretetype(Tensor{Tuple{@Symmetry{3,3}}, Float64})falsejulia> isconcretetype(Tensor{Tuple{@Symmetry{3,3}}, Float64, 2, 6})true
For example, prefer the fully specified field type in code that stores tensor values:
struct MaterialState{T}
σ::Tensor{Tuple{@Symmetry{3,3}}, T, 2, 6} # same as SymmetricSecondOrderTensor{3,T,6}
endFor fixed-size aliases, the same rule applies. SymmetricSecondOrderTensor{3,T} is convenient in method signatures, but a concrete field type also needs the component-count parameter L, as in SymmetricSecondOrderTensor{3,T,6}. For the Julia background, see Avoid fields with abstract type in the Julia manual.
Put known symmetry in the type
If a value is symmetric, represent that in the tensor type. The tensor still indexes and displays like the full tensor, but only the independent components are stored:
julia> A = @Mat [1.0 2.0 3.0; 2.0 4.0 5.0; 3.0 5.0 6.0]3×3 Tensor{Tuple{3, 3}, Float64, 2, 9}: 1.0 2.0 3.0 2.0 4.0 5.0 3.0 5.0 6.0julia> S = SymmetricSecondOrderTensor{3}(A)3×3 SymmetricSecondOrderTensor{3, Float64, 6}: 1.0 2.0 3.0 2.0 4.0 5.0 3.0 5.0 6.0julia> Tuple(S)(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)
This also tells later operations which tensor space to use:
julia> gradient(identity, S) isa SymmetricFourthOrderTensor{3}true
Use symmetric when you want to compute the symmetric part of a general second-order tensor. For the full notation, see Tensor Types and Spaces.
Specify @einsum result types when needed
@einsum infers free indices, but it cannot prove every symmetry of the result. If you know the result belongs to a symmetric tensor space, give that space explicitly:
julia> A = rand(Mat{3,3});julia> S1 = @einsum A[k,i] * A[k,j]3×3 Tensor{Tuple{3, 3}, Float64, 2, 9}: 1.4203 0.875685 1.58267 0.875685 1.19535 1.39938 1.58267 1.39938 2.13089julia> S1 isa SymmetricSecondOrderTensor{3}falsejulia> S2 = @einsum SymmetricSecondOrderTensor{3} A[k,i] * A[k,j]3×3 SymmetricSecondOrderTensor{3, Float64, 6}: 1.4203 0.875685 1.58267 0.875685 1.19535 1.39938 1.58267 1.39938 2.13089julia> S2 isa SymmetricSecondOrderTensor{3}truejulia> S1 ≈ S2true
Tensorial uses the annotated type as the result space. Use it only when the formula really has that symmetry.
Flatten at boundaries
Tensorial code usually reads best when tensor values stay as tensors. Use flatview, tovoigt, tomandel, and the corresponding inverse conversions at boundaries, such as solver interfaces, file formats, or code that specifically expects vectors and matrices:
julia> σ = SymmetricSecondOrderTensor{3}((2.0, 0.4, 0.2, 1.2, 0.1, 0.9))3×3 SymmetricSecondOrderTensor{3, Float64, 6}: 2.0 0.4 0.2 0.4 1.2 0.1 0.2 0.1 0.9julia> v = tovoigt(σ)6-element StaticArraysCore.SVector{6, Float64} with indices SOneTo(6): 2.0 1.2 0.9 0.1 0.2 0.4julia> fromvoigt(SymmetricSecondOrderTensor{3}, v) ≈ σtrue
For symmetric tensor blocks inside direct sums, flatview uses Mandel scaling. See Voigt Form for the conversion rules.
Type inference when unpacking
unpack(x) returns all blocks of a direct-sum value as a tuple. The return type is concrete because the full block layout is known. The @code_warntype excerpts below omit the long inferred-code body and keep the parts relevant to type inference. In each excerpt, focus on the Body::... line: it shows the return type Julia inferred for the call.
For unpack(x), Julia knows the complete block layout and infers the concrete tuple type:
julia> x = pack(σ, 0.2)
2-element DirectSumVector with storage Float64:
Space(Symmetry(3, 3),)
Space()
julia> @code_warntype unpack(x)
MethodInstance for unpack(::DirectSumVector{...})
Arguments
#self#::Core.Const(Tensorial.unpack)
A::DirectSumVector{...}
Body::Tuple{SymmetricSecondOrderTensor{3, Float64, 6}, Float64}For indexed access, the method receives the block index as an Int. Since the blocks can have different types, Julia infers a small Union:
julia> @code_warntype unpack(x, 1)
MethodInstance for unpack(::DirectSumVector{...}, ::Int64)
Arguments
#self#::Core.Const(Tensorial.unpack)
A::DirectSumVector{...}
i::Int64
Body::Union{Float64, SymmetricSecondOrderTensor{3, Float64, 6}}If the selected block is written as a constant inside a function, Julia can propagate that constant and infer the concrete block type:
julia> first_block(x) = unpack(x, 1);
julia> @code_warntype first_block(x)
MethodInstance for first_block(::DirectSumVector{...})
Arguments
#self#::Core.Const(first_block)
x::DirectSumVector{...}
Body::SymmetricSecondOrderTensor{3, Float64, 6}Use indexed unpack for inspection or when the selected block is known to the compiler. Otherwise, prefer unpack(x) and destructure the returned tuple. For the full output, see the @code_warntype documentation.